Skip to content

fix(process): reap entire process group on cmd.Cancel - #39143

Merged
bircni merged 11 commits into
go-gitea:mainfrom
rremer:main-orphanage
Aug 29, 2026
Merged

fix(process): reap entire process group on cmd.Cancel#39143
bircni merged 11 commits into
go-gitea:mainfrom
rremer:main-orphanage

Conversation

@rremer

@rremer rremer commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

I ran into an issue where many client disconnects during clone operations were leaving expensive git pack-objects --revs --thin --stdout --delta-base-offset processes in gitea. These processes would still run to completion, but the underlying http request which initiated them had long since been cancelled.

The bug is subtle, but the cmd.Cancel call would target the original PID, while git commands regularly fork several child processes which are not cancelled. Because their parent was cancelled, they'd get reparented to either the gitea process or pid 1 depending on the kernel.

This change uses the negative process group to cancel the entire process group together instead of just the parent.

@GiteaBot GiteaBot added the lgtm/need 2 This PR needs two approvals by maintainers to be considered for merging. label Aug 26, 2026
Comment thread modules/process/manager_unix.go Outdated
@wxiaoguang
wxiaoguang marked this pull request as draft August 26, 2026 18:32
Comment thread modules/process/manager_unix_test.go Outdated
@silverwind

Copy link
Copy Markdown
Member

Please trim down on comments.

…arent

Assisted-by: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
Signed-off-by: Royce Remer <royceremer@gmail.com>
Comment thread modules/process/manager_unix_test.go Outdated
@wxiaoguang

Copy link
Copy Markdown
Contributor

What do you think about this bc21fdd ?

@wxiaoguang
wxiaoguang marked this pull request as ready for review August 27, 2026 10:18
wxiaoguang and others added 2 commits August 27, 2026 18:38
…escalate SIGINT -> SIGKILL

Assisted-by: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
Signed-off-by: Royce Remer <royceremer@gmail.com>
@rremer

rremer commented Aug 27, 2026

Copy link
Copy Markdown
Contributor Author

@wxiaoguang while a much bigger refactor, your change will be safer because mine relied on callers to use setSysProcAttribute(), whereas now it would be the default behavior. The move to SIGTERM is also preferable, however I believe it introduced a new bug: if the child ignores SIGTERM it could hang forever, and grandchildren would never get a signal.

I've added a few test cases to prove this, and my proposed fix is just to introduce a default 10s (configurable) wait on Cancel() before escalating to SIGKILL: 9e336b1

@wxiaoguang

wxiaoguang commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

however I believe it introduced a new bug: if the child ignores SIGTERM it could hang forever, and grandchildren would never get a signal.

I've added a few test cases to prove this, and my proposed fix is just to introduce a default 10s (configurable) wait on Cancel() before escalating to SIGKILL: 9e336b1

I intentionally didn't do that because it will just cause pid data-race problems. You can ask AI about the PID-race details.

So I strongly prefer to revert 9e336b1 (Actually, I mean, it needs to be reverted)

And, I don't see why the SIGTERM would be ignored in real world for our cases. If it would happen, I already prepared WithOnCancelForceKill

Comment thread modules/process/command_unix.go Outdated
@wxiaoguang
wxiaoguang marked this pull request as draft August 27, 2026 17:19
Assisted-by: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
Signed-off-by: Royce Remer <royceremer@gmail.com>
@rremer

rremer commented Aug 27, 2026

Copy link
Copy Markdown
Contributor Author

pid data-race problems.

Good point, although technically SIGINT suffers from this as well.

I don't see why the SIGTERM would be ignored in real world for our cases

Maybe not 'ignored' in real-world scenarios, but certainly blocked longer than what we might be willing to wait. How long is too-long is really only known by the caller, but I could see scenarios where that external renderer or ssh call sites hang on some connection. Is 5 minutes too-long? Is 30 seconds?

It looks like ProcessState can be trusted while we hold a reference, how about we just check it before doing any syscall? 3b7449f

@wxiaoguang

Copy link
Copy Markdown
Contributor

I don't see why the SIGTERM would be ignored in real world for our cases

Maybe not 'ignored' in real-world scenarios, but certainly blocked longer than what we might be willing to wait. How long is too-long is really only known by the caller, but I could see scenarios where that external renderer or ssh call sites hang on some connection. Is 5 minutes too-long? Is 30 seconds?

It looks like ProcessState can be trusted while we hold a reference, how about we just check it before doing any syscall? 3b7449f

I don't see it would really affect real world use cases. And there is no easy solution for the PID reuse data-race.

I believe we can take the graceful kill (SIGTERM) as default behavior, if anything wrong, just switch to SIGKILL.

@wxiaoguang

wxiaoguang commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

It looks like ProcessState can be trusted while we hold a reference, how about we just check it before doing any syscall? 3b7449f

No, it's not right. "there is no easy solution for the PID reuse data-race."

@rremer

rremer commented Aug 27, 2026

Copy link
Copy Markdown
Contributor Author

I don't see it would really affect real world use cases

I think the real-world use-case is pretty theoretical, for sure, but this would reduce the window of pid reuse from however long since the process was started, to probably nanoseconds.

@wxiaoguang
wxiaoguang marked this pull request as ready for review August 27, 2026 18:39
@wxiaoguang

wxiaoguang commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

I don't see it would really affect real world use cases

I think the real-world use-case is pretty theoretical, for sure, but this would reduce the window of pid reuse from however long since the process was started, to probably nanoseconds.

Let's just try to use SIGTERM as the default signal in production to see whether there would be real problems. If yes, just switch to SIGKILL.

@GiteaBot GiteaBot added lgtm/need 1 This PR needs approval from one additional maintainer to be merged. and removed lgtm/need 2 This PR needs two approvals by maintainers to be considered for merging. labels Aug 27, 2026
@bircni
bircni requested a review from silverwind August 28, 2026 20:59
@GiteaBot GiteaBot added lgtm/done This PR has enough approvals to get merged. There are no important open reservations anymore. and removed lgtm/need 1 This PR needs approval from one additional maintainer to be merged. labels Aug 29, 2026
@bircni
bircni enabled auto-merge (squash) August 29, 2026 19:36
@bircni bircni added the reviewed/wait-merge This pull request is part of the merge queue. It will be merged soon. label Aug 29, 2026
@bircni
bircni merged commit eea0367 into go-gitea:main Aug 29, 2026
24 checks passed
@GiteaBot GiteaBot added this to the 28.0.0 milestone Aug 29, 2026
@GiteaBot GiteaBot removed the reviewed/wait-merge This pull request is part of the merge queue. It will be merged soon. label Aug 29, 2026
silverwind added a commit to silverwind/gitea that referenced this pull request Aug 30, 2026
* origin/main: (30 commits)
  docs: Update CHANGELOG for release 1.27.3 (go-gitea#39170)
  [skip ci] Updated translations via Crowdin
  [skip ci] Updated translations via Crowdin
  fix(process): reap entire process group on cmd.Cancel (go-gitea#39143)
  feat(web): Add org removal functionality to admin user details page (go-gitea#38013)
  fix(actions): run every due schedule exactly once per occurrence (go-gitea#39078)
  refactor: pagination/pager (go-gitea#39162)
  [skip ci] Updated translations via Crowdin
  enhance(actions): make workflow dispatch choice dropdown support search (go-gitea#39154)
  fix(web): populate the reason for "cannot commit to branch" in web editor commit form (go-gitea#39155)
  refactor(automerge): fix error handling, populate recent automerge tasks on restart (go-gitea#39001)
  chore(frontend): avoid loading CSS twice in vite dev mode (go-gitea#39160)
  fix(packages): preserve SemVer prerelease identifiers in Swift Registry (go-gitea#39156)
  [skip ci] Updated translations via Crowdin
  ci(snap): pack snaps without an LXD container (go-gitea#39152)
  chore: apply golangci "forbidigo" to all packages (go-gitea#39151)
  refactor: drop two unmaintained dependencies, rename the byte size helpers (go-gitea#39083)
  fix(actions): keep step-level continue-on-error expressions unevaluated (go-gitea#39141)
  [skip ci] Updated translations via Crowdin
  feat: add deploy tokens (go-gitea#37306)
  ...

# Conflicts:
#	modelmigration/migrations.go
#	modelmigration/v28/v352.go
#	routers/private/hook_pre_receive.go
#	templates/repo/release/new.tmpl
@rremer

rremer commented Aug 31, 2026

Copy link
Copy Markdown
Contributor Author

FYI that this may need a follow-up, it was decided not to use SIGKILL, and with SIGINT this still leaves child processes like pack-objects --revs --thin --stdout --progress --delta-base-offset running on gitea. The way I replicated this was to execute a git clone, then SIGINT my git clone process. You can see on gitea it spawning git-remote-http (in my case, maybe with ssh this bug doesn't exist?) and the children. The git-remote-http disappears after killing the client's git-clone process, but the children still persist.

@wxiaoguang

wxiaoguang commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

FYI that this may need a follow-up, it was decided not to use SIGKILL, and with SIGINT this still leaves child processes like pack-objects --revs --thin --stdout --progress --delta-base-offset running on gitea.

Can these processes be killed by SIGTERM?

  • If yes: need to figure why they are not killed, because we already sent SIGTERM to their process groups
  • If no: then we have to use SIGKILL (I doubt about it, see below, pack-objects process can be killed gracefully)

You can see on gitea it spawning git-remote-http (in my case, maybe with ssh this bug doesn't exist?) and the children.

Are you sure git-remote-http is spawned by Gitea?

Some details are still unclear to me because it seems that the problem you described is not that what should happen.

Two AIs say: git-remote-http runs on the client side.

Details image image

@wxiaoguang

wxiaoguang commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

Also, I have verified, git pack-objects can be killed by SIGTERM when it is running.

You can try it by: run git pack-objects HEAD in one terminal, then run kill ... in another terminal.

So, I don't think the git pack-objects processes you saw could be related to the SIGTERM / SIGKILL (suppose the new code has sent SIGTERM to their process groups)

@wxiaoguang

Copy link
Copy Markdown
Contributor

Also, you can set c.termGraceful = false in process.CommandContext, then it should be exactly the same as the first commit in this PR 8686ec5 : syscall.Kill(-cmd.Process.Pid, syscall.SIGKILL), to see whether there would be differences.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

lgtm/done This PR has enough approvals to get merged. There are no important open reservations anymore. topic/code-linting type/bug

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants